#define _GNU_SOURCE
#include <X11/Xft/Xft.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/extensions/Xinerama.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>

#define SchemeNorm 0
#define SchemeSel 1
#define SchemeLast 2

static const char *colors[SchemeLast][2] = {
    [SchemeNorm] = {"#ffffff", "#222222"},
    [SchemeSel]  = {"#000000", "#ffff00"},
};

static const char *font_names[] = {
    "DejaVu Sans Mono:size=20:antialias=true:hinting=true",
    "Monaco:size=20:antialias=true:hinting=true",
    "Consolas:size=20:antialias=true:hinting=true",
    "monospace:size=20:antialias=true:hinting=true",
    NULL
};

static unsigned int win_w = 1280, win_h = 720;
static int curr_offset = 0;
static int sel_idx = 0;
static int items_per_page = 0;

typedef struct {
    char text[1024];
    char path[1024];
    unsigned long long bytes;
} Entry;
static Entry *entries = NULL;
static int entry_count = 0;

static int compare_entries(const void *arg1, const void *arg2) {
    Entry *a = (Entry*)arg1;
    Entry *b = (Entry*)arg2;
    if (b->bytes > a->bytes) return 1;
    if (b->bytes < a->bytes) return -1;
    return 0;
}

unsigned long long size_str_to_bytes(const char *size_str) {
    char *endptr;
    double num = strtod(size_str, &endptr);
    unsigned long long bytes = 0;
    switch (tolower(*endptr)) {
        case 'k': bytes = num * 1024; break;
        case 'm': bytes = num * 1024 * 1024; break;
        case 'g': bytes = num * 1024 * 1024 * 1024; break;
        default: bytes = num; break;
    }
    return bytes;
}

void read_and_process_entries() {
    char line[1024];
    while (fgets(line, sizeof(line), stdin)) {
        line[strcspn(line, "\n")] = '\0';
        if (strlen(line) == 0) continue;

        char size_str[32], path[1024];
        if (sscanf(line, "%s %[^\n]", size_str, path) != 2) continue;
        if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) continue;

        unsigned long long bytes = size_str_to_bytes(size_str);
        entry_count++;
        entries = realloc(entries, entry_count * sizeof(Entry));
        Entry *e = &entries[entry_count - 1];
        strncpy(e->text, line, sizeof(e->text) - 1);
        strncpy(e->path, path, sizeof(e->path) - 1);
        e->bytes = bytes;
    }
    qsort(entries, entry_count, sizeof(Entry), compare_entries);
}

typedef struct {
    Display *dpy;
    unsigned int h;
    XftFont *xfont;
} Fnt;

enum { ColFg, ColBg };
typedef XftColor Clr;

typedef struct {
    unsigned int w, h;
    Display *dpy;
    int screen;
    Window root;
    Drawable drawable;
    GC gc;
    Clr *scheme;
    Fnt *font;
} Drw;

Drw *drw_create(Display *dpy, int screen, Window root) {
    Drw *drw = calloc(1, sizeof(Drw));
    drw->dpy = dpy;
    drw->screen = screen;
    drw->root = root;
    drw->w = win_w;
    drw->h = win_h;
    drw->drawable = XCreatePixmap(dpy, root, drw->w, drw->h, DefaultDepth(dpy, screen));
    drw->gc = XCreateGC(dpy, root, 0, NULL);
    return drw;
}

Fnt *font_create(Drw *drw) {
    Fnt *f = calloc(1, sizeof(Fnt));
    f->dpy = drw->dpy;
    XftFont *xfont = NULL;
    for (int i = 0; font_names[i]; i++) {
        xfont = XftFontOpenName(drw->dpy, drw->screen, font_names[i]);
        if (xfont) break;
    }
    if (!xfont) {
        fprintf(stderr, "Warning: Cannot load optimized font, use default monospace\n");
        xfont = XftFontOpenName(drw->dpy, drw->screen, "monospace:size=10");
        if (!xfont) {
            fprintf(stderr, "Error: Cannot load any font\n");
            free(f);
            return NULL;
        }
    }
    f->xfont = xfont;
    f->h = xfont->ascent + xfont->descent;
    return f;
}

void drw_clr_create(Drw *drw, Clr *dest, const char *name) {
    if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen),
                          DefaultColormap(drw->dpy, drw->screen), name, dest))
        fprintf(stderr, "Cannot allocate color: %s\n", name);
}

void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled) {
    XSetForeground(drw->dpy, drw->gc, drw->scheme[ColBg].pixel);
    if (filled) XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h);
    else XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w-1, h-1);
}

void drw_text(Drw *drw, int x, int y, const char *text) {
    char ascii_text[1024];
    int idx = 0;
    for (int i = 0; text[i] && idx < sizeof(ascii_text)-1; i++) {
        if (text[i] >= 0x20 && text[i] <= 0x7E) {
            ascii_text[idx++] = text[i];
        }
    }
    ascii_text[idx] = '\0';

    XftDraw *d = XftDrawCreate(drw->dpy, drw->drawable,
                            DefaultVisual(drw->dpy, drw->screen),
                            DefaultColormap(drw->dpy, drw->screen));
    int ty = y + (drw->font->h / 2) + (drw->font->xfont->ascent / 2);

    XGlyphInfo ext;
    XftTextExtentsUtf8(drw->dpy, drw->font->xfont, (XftChar8 *)ascii_text, strlen(ascii_text), &ext);
    if (ext.xOff > drw->w - 20) {
        char truncated[1024];
        strncpy(truncated, ascii_text, 250);
        truncated[250] = '\0';
        strcat(truncated, "...");
        XftDrawStringUtf8(d, &drw->scheme[ColFg], drw->font->xfont, x, ty, (XftChar8 *)truncated, strlen(truncated));
    } else {
        XftDrawStringUtf8(d, &drw->scheme[ColFg], drw->font->xfont, x, ty, (XftChar8 *)ascii_text, strlen(ascii_text));
    }

    XftDrawDestroy(d);
}

void drw_refresh(Drw *drw, Window win) {
    XCopyArea(drw->dpy, drw->drawable, win, drw->gc, 0, 0, drw->w, drw->h, 0, 0);
    XSync(drw->dpy, False);
}

void draw_list(Drw *drw, Clr *norm, Clr *sel) {
    items_per_page = drw->h / drw->font->h;
    if (items_per_page <= 0) items_per_page = 1;

    drw->scheme = norm;
    drw_rect(drw, 0, 0, drw->w, drw->h, 1);

    int y = 0;
    for (int i = curr_offset; i < entry_count && i < curr_offset + items_per_page; i++) {
        drw->scheme = (i == sel_idx) ? sel : norm;
        drw_rect(drw, 0, y, drw->w, drw->font->h, 1);
        drw_text(drw, 10, y, entries[i].text);
        y += drw->font->h;
    }

    char status[64];
    int total_pages = (entry_count + items_per_page - 1) / items_per_page;
    int curr_page = (curr_offset / items_per_page) + 1;
    snprintf(status, 64, "Loop Scroll | Page %d/%d | Total %d Items", curr_page, total_pages, entry_count);
    drw->scheme = sel;
    drw_rect(drw, 0, drw->h - drw->font->h, drw->w, drw->font->h, 1);
    drw_text(drw, 10, drw->h - drw->font->h, status);
}

void handle_mouse(Drw *drw, Clr *norm, Clr *sel, XButtonEvent *ev) {
    if (ev->button == 4) {
        sel_idx = (sel_idx == 0) ? entry_count - 1 : sel_idx - 1;
        curr_offset = sel_idx - (sel_idx % items_per_page);
        draw_list(drw, norm, sel);
        drw_refresh(drw, ev->window);
    } else if (ev->button == 5) {
        sel_idx = (sel_idx == entry_count - 1) ? 0 : sel_idx + 1;
        curr_offset = sel_idx - (sel_idx % items_per_page);
        draw_list(drw, norm, sel);
        drw_refresh(drw, ev->window);
    } else if (ev->button == 1) {
        int click_idx = curr_offset + (ev->y / drw->font->h);
        if (click_idx >= 0 && click_idx < entry_count) {
            printf("%s\n", entries[click_idx].path);
            exit(0);
        }
    }
}

void event_loop(Display *dpy, Window win, Drw *drw, Clr *norm, Clr *sel) {
    XEvent ev;
    while (1) {
        XNextEvent(dpy, &ev);
        switch (ev.type) {
            case Expose:
                if (ev.xexpose.count == 0) {
                    draw_list(drw, norm, sel);
                    drw_refresh(drw, win);
                }
                break;
            case ButtonPress:
                handle_mouse(drw, norm, sel, &ev.xbutton);
                break;
            case KeyPress: {
                KeySym ks = XLookupKeysym(&ev.xkey, 0);
                if (ks == XK_Escape) exit(0);
                break;
            }
            case DestroyNotify:
                exit(0);
        }
    }
}

Window create_window(Display *dpy) {
    int screen = DefaultScreen(dpy);
    Window root = RootWindow(dpy, screen);
    XSetWindowAttributes swa;

    int x = 0, y = 0;
    XineramaScreenInfo *info;
    int n;
    if ((info = XineramaQueryScreens(dpy, &n))) {
        XWindowAttributes wa;
        XGetWindowAttributes(dpy, root, &wa);
        x = (wa.width - win_w) / 2;
        y = (wa.height - win_h) / 2;
        XFree(info);
    }

    swa.override_redirect = True;
    swa.background_pixel = 0;
    swa.event_mask = ExposureMask | ButtonPressMask | DestroyNotify | KeyPressMask;
    Window win = XCreateWindow(dpy, root, x, y, win_w, win_h, 0,
                            DefaultDepth(dpy, screen), InputOutput,
                            DefaultVisual(dpy, screen),
                            CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
    XMapRaised(dpy, win);
    return win;
}

void usage() {
    fprintf(stderr, "Usage: command | dumenu [OPTIONS]\n");
    fprintf(stderr, "List view for du -ha output (English only, anti-aliased)\n");
    fprintf(stderr, "Examples: du -ha | dumenu\n");
    fprintf(stderr, "Options:\n");
    fprintf(stderr, "  -h        Show this help\n");
    fprintf(stderr, "  -w <width> Set window width (default: 1280px)\n");
    fprintf(stderr, "  -H <height> Set window height (default: 720px)\n");
    exit(0);
}

int main(int argc, char **argv) {
    for (int i = 1; i < argc; i++) {
        if (!strcmp(argv[i], "-h")) usage();
        else if (!strcmp(argv[i], "-w") && i + 1 < argc) win_w = atoi(argv[++i]);
        else if (!strcmp(argv[i], "-H") && i + 1 < argc) win_h = atoi(argv[++i]);
        else usage();
    }

    read_and_process_entries();
    if (entry_count == 0) {
        fprintf(stderr, "Error: No valid entries\n");
        fprintf(stderr, "Example: du -ha | dumenu\n");
        return 1;
    }

    Display *dpy = XOpenDisplay(NULL);
    if (!dpy) {
        fprintf(stderr, "Error: Cannot open X display\n");
        return 1;
    }

    int screen = DefaultScreen(dpy);
    Window root = RootWindow(dpy, screen);
    Drw *drw = drw_create(dpy, screen, root);
    drw->font = font_create(drw);
    if (!drw->font) return 1;

    Clr norm[2], sel[2];
    drw_clr_create(drw, &norm[ColFg], colors[SchemeNorm][ColFg]);
    drw_clr_create(drw, &norm[ColBg], colors[SchemeNorm][ColBg]);
    drw_clr_create(drw, &sel[ColFg], colors[SchemeSel][ColFg]);
    drw_clr_create(drw, &sel[ColBg], colors[SchemeSel][ColBg]);

    Window win = create_window(dpy);
    draw_list(drw, norm, sel);
    drw_refresh(drw, win);
    event_loop(dpy, win, drw, norm, sel);

    free(entries);
    XCloseDisplay(dpy);
    return 0;
}
